<svelte:window on:resize="measure()" />
<Radar bind:ready />


{#if ready}
  <AtlasDataLoader 
    {id} 
    {layer} 
    {layout}
    {classFilter}
    {filter}
    bind:config
    bind:layers 
    bind:labels 
    bind:loading
  />
{/if}

<div ref:root
>
  <D3Zoom ref:d3Zoom
    clientWidth={viewWidth}
    clientHeight={viewHeight}
    bind:scale
    bind:translateX
    bind:translateY
    bind:mouseOver
    bind:mouseGlobalPosition
    bind:extent
    bind:scrollWheel
    bind:disableBehaviors
    bind:gcx
    bind:gcy
    {homeX}
    {homeY}
    {homeScale}
  >
    <canvas ref:canvas
      width={viewWidth * screenResolution}
      height={viewHeight * screenResolution}
    ></canvas>
    <canvas ref:labelsCanvas
      width={viewWidth * screenResolution}
      height={viewHeight * screenResolution}
    ></canvas>
    {#if showHoverIcon}
    <div ref:hover
      style="left: {hoverIconX}px; top: {hoverIconY}px; width: {hoverIconW}px; height: {hoverIconW}px;"
    ></div>
    {/if}
  </D3Zoom>
</div>

<script>
import { default as load } from './library/load.js';
import { max } from 'd3-array';

export default {
  components: { 
    AtlasDataLoader: './AtlasDataLoader.html', 
    D3Zoom: './library/D3Zoom.html',
    Radar: './library/Radar.html'
  },
  data() {
    return {
      ready: true,
      id: "inceptionv1_mixed4c",

      // viewWidth: 500,
      // viewHeight: 500,
      
      config: null,
      layers: null,

      layer: 0,
      layout: 0,
      classFilter: 0,
      filter: 0,

      context: null,

      alphaAttributionFactor: 0.02,
      density: 1.0,
      classHeatmap: -1,
      classHeatmapMultiplier: 1,
      classHeatmapPositive: 1,
      iconCrop: 0.02,
      autoGridSizeMultiplier: 0.5,

      gridSize: null,

      // for initial state, and going back to "home"
      homeX: .5,
      homeY: .5,
      homeScale: 1,

      // turn off features
      enableClickToZoom: true,
      enableHover: true,
      enableDragToPan: true,

      // Styling
      backgroundColor: "white",
      strokeColor: "rgb(220, 220, 220)",
      strokeThickness: 1,
      imageSmoothing: false,
      fontSize: 10,
      textColor: "white",
      textShadowColor: "rgba(0, 0, 0, 0.6)",
      showLabels: true,

      screenResolution: 1,
    }
  },
  computed: {
    maxAttributionValue: ({layers, layer}) => {
      if (layers == null) return 0;
      const l = layers[layer];
      let max = 0;
      l.forEach(x => {
        x.forEach(y => {
          if (y && y.num_activations > 500) {
            const v = y.full_class_values[0];
            if (v > max) max = v;
          }
        })
      })
      return max;
    },
    w: ({viewWidth, screenResolution}) => viewWidth * screenResolution,
    h: ({viewHeight, screenResolution}) => viewHeight * screenResolution,
    currentZoomIndex: ({scale, gridSize, config, classHeatmap, w, h, autoGridSizeMultiplier}) => {
      let s = 0;
      if (gridSize > -1) {
        s = +gridSize
      } else {
        const size = Math.max(w, h);
        const optimalNumIcons = ((size * scale) / 80) / autoGridSizeMultiplier;
        const snapToGrid = Math.floor(Math.sqrt(optimalNumIcons / 20));
        s = snapToGrid;
      }
      // Make sure we don't overrun our data
      if (config) {
        s = Math.min(config.grid_size.length - 1 , s);
      }
      // Class heatmap only has data up to level 2
      if (classHeatmap > -1) {
        s = Math.min(2, s);
      }
      return s;

    },
    // visibleLayers: ({currentZoomIndex}) => {
    //   return [{i: currentZoomIndex, opacity: 1}];
    // },
    currentLayerData: ({currentZoomIndex, layers}) => {
      return layers ? layers[currentZoomIndex] : [[]]
    },
    labelsBuffer: ({labelsBufferContext, labels, fontSize, textColor, textShadowColor}) => {
      // console.log(labelsBufferContext, labels);
    
      if (labelsBufferContext && labels) {
        labelsBufferContext.globalAlpha = 1.0;
        labelsBufferContext.font=fontSize + "px Helvetica";
        labels.forEach((label, i) => {
          // if (textShadow) {
          labelsBufferContext.lineWidth = 3;
          labelsBufferContext.lineJoin = "round"
          labelsBufferContext.strokeStyle = textShadowColor;
          labelsBufferContext.strokeText(label, (i % 10) * 150 + 10, Math.floor(i / 10 + 1) * 20, 80, 15);
          // }
          labelsBufferContext.fillStyle = textColor;
          labelsBufferContext.fillStyle = textColor;
          labelsBufferContext.fillText(label, (i % 10) * 150 + 10, Math.floor(i / 10 + 1) * 20, 80, 15);
                        
        });
      }
    },
    showHover: ({hoverIconData, mouseGlobalPosition}) => mouseGlobalPosition && hoverIconData && hoverIconData.gcx,
    hoverIconData: ({currentLayerData, config, mouseGlobalPosition, w, h, translateX, translateY}) => {
      // const msx = mouseGlobalPosition[0]
      // const msy = mouseGlobalPosition[1]
      if (currentLayerData) {
        const numGridRows = currentLayerData.length;
        if (mouseGlobalPosition) {
          const gx = Math.floor(mouseGlobalPosition[0] / Math.min(w, h) * numGridRows);
          const gy = Math.floor(mouseGlobalPosition[1] / Math.min(w, h) * numGridRows);
          if (currentLayerData[gy] && currentLayerData[gy][gx]) {
            return currentLayerData[gy][gx];
          } else {
            return {};
          }
        } else {
          return {};
        }
      } else {
        return {};
      }
    },
    hoverIconX: ({hoverIconData, scale, w, h, translateX}) => hoverIconData.gy * scale * Math.min(w, h) + translateX,
    hoverIconY: ({hoverIconData, scale, w, h, translateY}) => hoverIconData.gx * scale * Math.min(w, h) + translateY,
    hoverIconW: ({hoverIconData, scale, w, h}) => hoverIconData.gw * scale * Math.min(w, h),
    showHoverIcon: ({mouseGlobalPosition, hoverIconData, enableHover}) => {
      return enableHover && mouseGlobalPosition && hoverIconData && hoverIconData.gw
    },
  },
  onupdate({changed, current, previous}) {
    // console.log("atlas", changed, current.scale)
    if (!current.context || changed.viewWidth || changed.viewHeight) {
      const labelsContext = this.refs.labelsCanvas.getContext('2d');
      labelsContext.imageSmoothingEnabled = false;
      this.set({
        context: this.refs.canvas.getContext('2d'),
        labelsContext
      });
    }
    if (changed.loading || changed.autoGridSizeMultiplier || changed.labels || changed.density || changed.maxAttributionValue || changed.classHeatmap || changed.classHeatmapMultiplier || changed.classHeatmapPositive || changed.showLabels || changed.viewWidth || changed.viewHeight || changed.scale || changed.translateX || changed.translateY || changed.iconCrop || changed.gridSize || changed.layers) {
      this.render();
    }
    if (changed.hoverIconData) {
      const {tooltip} = this.store.get();
      const {showHoverIcon} = this.get();
      if (showHoverIcon) {
        tooltip.show(current.hoverIconData);
      } else {
        tooltip.hide();
      }
    }
    if (changed.showHoverIcon) {
      if (current.showHoverIcon == false) {
        const { tooltip } = this.store.get();
        tooltip.hide();
      }
    }

  },
  oncreate() {
    // Turn off tooltips while zooming
    const {tooltip} = this.store.get();
    this.refs.d3Zoom.on("zoom", () => {
      tooltip.hide();
    });
    // Offscreen buffer for drawing text labels;
    const labelsBufferCanvas = document.createElement("canvas");
    labelsBufferCanvas.width = 150 * 10;
    labelsBufferCanvas.height = (Math.ceil(1002 / 10) + 1) * 20;
    const labelsBufferContext = labelsBufferCanvas.getContext("2d");
    this.set({labelsBufferCanvas, labelsBufferContext});
    setTimeout(() => {
      this.measure();
      this.home();
    }, 10);
  },
  methods: {
    measure() {
      this.set({
        viewWidth: this.refs.root.offsetWidth,
        viewHeight: this.refs.root.offsetHeight,
      })
    },
    fullscreen() {
      this.refs.root.webkitRequestFullscreen();
    },
    home(duration=0) {
      // const {homeX, homeY, homeScale} = this.get();
      // this.transitionTo(homeX, homeY, homeScale, duration);
      this.refs.d3Zoom.home(duration);
    },
    translateTo(x, y) {
      this.refs.d3Zoom.translateTo(x, y);
    },
    zoomTo(x, y, scale, duration = 1000) {
      this.refs.d3Zoom.zoomTo(x, y, scale, duration);
    },
    transitionTo(x, y, scale, duration=0) {
      this.refs.d3Zoom.transformTo(x, y, scale, duration);
    },
    scaleBy(multiplier) {
      const { scale } = this.get();
      this.refs.d3Zoom.scaleTo(scale * multiplier, 300);
    },
    iconToGlobalPosition(icon, layerIndex) {
      const {density, scale, translateX, translateY, config, w, h} = this.get();
      const gridSize = config.grid_size[layerIndex];
      const gridWidth = config.icon_size * gridSize;

      const iconWidth = icon.gw * scale * Math.min(w, h);

      // x, y swapped intentionally
      const iconX = icon.gy * scale * Math.min(w, h) + translateX;
      const iconY = icon.gx * scale * Math.min(w, h) + translateY;

      const sourceX = icon.localX * config.icon_size;
      const sourceY = icon.localY * config.icon_size;

      const totalSamples = (typeof config.filter[0] == "number" ? config.filter[0] : config.sample_images)
      const avgSamples = totalSamples / (gridSize * gridSize);

      // Resize based on density
      const relativeDensity = Math.min(1, Math.sqrt(density * icon.num_activations / avgSamples));
      const adjustedIconWidth = iconWidth * relativeDensity;
      const adjustedIconX = iconX + (iconWidth - adjustedIconWidth) / 2;
      const adjustedIconY = iconY + (iconWidth - adjustedIconWidth) / 2

      return {sourceX, sourceY, iconX: adjustedIconX, iconY: adjustedIconY, iconWidth: adjustedIconWidth}
    },
    clear() {
      const {viewHeight, viewWidth, context, backgroundColor, labelsContext} = this.get();
      context.globalAlpha = 1;
      context.fillStyle= backgroundColor;
      context.clearRect(0, 0, viewWidth, viewHeight);
      context.fillRect(0, 0, viewWidth, viewHeight);
      labelsContext.globalAlpha = 1;
      labelsContext.clearRect(0, 0, viewWidth, viewHeight);
    },
    render() {

      const {id, labelsContext, loading, classHeatmap, showLabels, labels, labelsBufferCanvas, imageSmoothing, scale, w, h, translateX, translateY, context, backgroundColor, config, layers, visibleLayers, currentZoomIndex, strokeColor, strokeThickness, fontSize,textShadowColor, textColor, maxAttributionValue, classHeatmapMultiplier} = this.get();

      this.clear();
      // context.imageSmoothingQuality = "low";
      context.imageSmoothingEnabled = imageSmoothing;

      labelsContext.strokeStyle = strokeColor;
      labelsContext.lineWidth = strokeThickness;
      context.fillStyle = "white";

      if (config && layers && !loading) {
        let visibleLayers = [
          {i: currentZoomIndex, opacity: 1.0}
        ];
        this.set({visibleLayers});

        visibleLayers.forEach(visibleLayer => {
          const layerIndex = visibleLayer.i;
          const layerOpacity = visibleLayer.opacity;
          const icons = layers[layerIndex];

            
            // Calculating min and max indices for icon render loop
            const minSize = Math.min(w, h);
            const minX = Math.max(0, Math.floor(-(translateX / scale / minSize) * icons.length));
            const maxX = minX + Math.ceil(( w / scale / minSize) * icons.length);
            const minY = Math.max(0, Math.floor(-(translateY / scale / minSize) * icons.length));
            const maxY = minY + Math.ceil(( h / scale / minSize) * icons.length);

            for (let y = minY; y <= maxY; y++) {
              for (let x = minX; x <= maxX; x++) {
                
                if (icons[y] && icons[y][x]) {
                  const icon = icons[y][x];

                  const {sourceX, sourceY, iconX, iconY, iconWidth} = this.iconToGlobalPosition(icon, layerIndex);
                  
                  const requestedID = id;
                  // If icon is in the viewport 
                  // probably don't need this anymore
                  // if (iconX > -iconWidth && iconX < w && iconY > -iconWidth && iconY < h) {
                  if (true) {
                    
                    // We want to draw a box so there isn't just whiteness.
                    if (classHeatmap > -1 || true) {
                      labelsContext.globalAlpha = 0.3;
                      labelsContext.strokeRect(iconX, iconY, iconWidth, iconWidth);
                      labelsContext.globalAlpha = 1.0;
                    }
                    const textSkipX = Math.ceil(40 / iconWidth);
                    const textSkipY = Math.ceil(60 / iconWidth);
                    if (showLabels && labels && classHeatmap === -1 && icon.y % textSkipY == 0 && icon.x % textSkipX == 0 ) {
                      labelsContext.globalAlpha = 1;
                      const labelIndex = icon.top_class_indices[0];
                      labelsContext.drawImage(
                        labelsBufferCanvas,
                        //source
                        (labelIndex % 10) * 150, Math.floor(labelIndex / 10) * 20 + 4, Math.min(iconWidth * textSkipY - 5, 100), 20,
                        //destination
                        iconX, iconY + iconWidth - 20 - 2, Math.min(iconWidth * textSkipY - 5, 100), 20
                      );
                    }

                    load(icon.url).then(response => {
                      
                      // check that we're still on the right layer/zoom/id
                      const {id, visibleLayers, iconCrop, showLabels, textShadow} = this.get();
                      // console.log(requestedID, id)

                      if(visibleLayers.reduce((acc, layer) => layer.i === layerIndex ? acc + 1 : acc, 0) && requestedID === id) {
                        const {labelsContext, labelsBufferCanvas, alphaAttributionFactor, labels, config, classHeatmap, classHeatmapMultiplier, classHeatmapPositive} = this.get();

                        const {sourceX, sourceY, iconX, iconY, iconWidth} = this.iconToGlobalPosition(icon, layerIndex);

                        // If we have a class heatmap active, calculate the transparency for the current icon
                        let a = 1;
                        if (classHeatmap > -1) {
                          let i = icon.full_class_indices.indexOf(classHeatmap);
                          if (i > -1) {
                            a = icon.full_class_values[i] / maxAttributionValue;
                            a = a * classHeatmapPositive;
                            a = Math.max(0, a) * classHeatmapMultiplier;
                          } else {
                            a = 0.0;
                          }
                          
                        }
                        const computedIconCrop = Math.min(8 / iconWidth, 0.6);
                        // draw the icon
                        context.globalAlpha = 1;
                        const iconOffset = (computedIconCrop * config.icon_size) / 2;
                        // context.clearRect(iconX + 1, iconY + 1, iconWidth - 2, iconWidth - 2);
                        if (classHeatmap > -1) {
                          // labelsContext.beginPath();
                          context.fillRect(iconX, iconY, iconWidth, iconWidth);
                          // context.fill();
                          // context.closePath();
                        }
                        context.globalAlpha = a;
                        context.drawImage(response,
                          //source
                          sourceY + iconOffset, sourceX + iconOffset, config.icon_size - iconOffset * 2, config.icon_size - iconOffset * 2,
                          //destination
                          iconX, iconY, iconWidth, iconWidth
                        );
                        
                        // context.globalAlpha = 1;
                        // context.closePath();
                        
                      }
                    })
                  }
                }
              }
            }
          });
        }
      }
    }
  }

</script>


<style>
  ref:root {
    position: relative;
    width: 100%;
    height: 100%;
    contain: layout;
    overflow: hidden;
  }
  ref:stage {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
    overflow: hidden;
  }
  ref:canvas, ref:labelsCanvas {
    pointer-events: none;
    position: absolute;
    top: 0;
    left: 0;
    border-radius: 8px;
  }
  ref:hover {
    position: absolute;
    top: 0;
    left: 0;
    border-radius: 4px;
    border: solid 3px black;
    pointer-events: none;
    box-sizing: border-box;
  }
</style>
